﻿namespace Bridge.Exporter.Maptools
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.IO;
    using System.Security;
    using System.Security.Cryptography;
    using System.Text;
    using System.Xml;
    using Bridge.Exporter.Utilities;
    using Ionic.Zip;
    using log4net;

    /// <summary>
    /// Class for creating a token and outputting it to a file.
    /// </summary>
    public class Token
    {
        /// <summary>
        /// Default constructor.
        /// </summary>
        public Token()
        {
            TokenType = TokenType.NPC;
        }

        /// <summary>
        /// Gets or sets the token image location.
        /// </summary>
        public FileInfo TokenImage { get; set; }

        /// <summary>
        /// Gets or sets the portrait image location.
        /// </summary>
        public FileInfo PortraitImage { get; set; }

        /// <summary>
        /// Gets or sets the name.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Gets or sets the sight type.
        /// </summary>
        public string SightType { get; set; }

        /// <summary>
        /// Gets or sets the label string.
        /// </summary>
        public string Label { get; set; }

        /// <summary>
        /// Gets or sets the notes string.
        /// </summary>
        public string Notes { get; set; }

        /// <summary>
        /// Gets or sets the GM notes string.
        /// </summary>
        public string GMNotes { get; set; }

        /// <summary>
        /// Gets or sets the GM name string.
        /// </summary>
        public string GMName { get; set; }

        /// <summary>
        /// Gets or sets thr PropertyType string.
        /// </summary>
        public string PropertyType { get; set; }

        /// <summary>
        /// Gets or sets if this token has sight or not.
        /// </summary>
        public bool HasSight { get; set; }

        /// <summary>
        /// Gets or sets the size map guid.
        /// </summary>
        public string SizeMapGuid { get; set; }

        /// <summary>
        /// Gets or sets the token type.
        /// </summary>
        public TokenType TokenType { get; set; }

        /// <summary>
        /// Gets or sets the properties of the token.
        /// </summary>
        public List<MaptoolsProperty> Properties { get; set; }

        /// <summary>
        /// Gets or sets the states of the token.
        /// </summary>
        public List<State> States { get; set; }

        /// <summary>
        /// Outputs the token to the specified <see cref="FileInfo"/>.
        /// </summary>
        /// <param name="outputFileInfo"><see cref="FileInfo"/> to output to.</param>
        /// <returns>True if output was successful; otherwise, false.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="outputFileInfo" />, Properties, States, TokenImage or PortraitImage is null.</exception>
        public bool Output(FileInfo outputFileInfo)
        {
            if (outputFileInfo == null) throw new ArgumentNullException("outputFileInfo");
            if (Properties == null) throw new ArgumentNullException("Properties");
            if (States == null) throw new ArgumentNullException("States");
            if (TokenImage == null) throw new ArgumentNullException("TokenImage");
            if (PortraitImage == null) throw new ArgumentNullException("PortraitImage");

            FileInfo temporaryRar = null;
            DirectoryInfo tokenDirectoryInfo = null;
            bool returnBool = false;
            try
            {
                DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetTempPath());
                tokenDirectoryInfo = directoryInfo.CreateSubdirectory("token" + DateTime.Now.ToString("MMddyyy-hhmmss") + Guid.NewGuid());
                FileInfo contentXmlFileInfo = new FileInfo(Path.Combine(tokenDirectoryInfo.FullName, "content.xml"));
                FileInfo propertiesXmlFileInfo = new FileInfo(Path.Combine(tokenDirectoryInfo.FullName, "properties.xml"));
                FileInfo thumbnailFileInfo = new FileInfo(Path.Combine(tokenDirectoryInfo.FullName, "thumbnail"));
                DirectoryInfo assetsDirectoryInfo = tokenDirectoryInfo.CreateSubdirectory("assets");

                string tokenImageMd5 = null;
                string fullPreviewImageMd5 = null;
                
                using (FileStream fileStream = File.OpenRead(TokenImage.FullName))
                using (MD5 MD5Getter = MD5.Create())
                {
                    byte[] data = MD5Getter.ComputeHash(fileStream);
                    StringBuilder stringBuilder = new StringBuilder();
                    foreach (byte theByte in data)
                    {
                        stringBuilder.Append(theByte.ToString("x2"));
                    }
                    tokenImageMd5 = stringBuilder.ToString();
                }

                using (FileStream fileStream = File.OpenRead(PortraitImage.FullName))
                using (MD5 MD5Getter = MD5.Create())
                {
                    byte[] data = MD5Getter.ComputeHash(fileStream);
                    StringBuilder stringBuilder = new StringBuilder();
                    foreach (byte theByte in data)
                    {
                        stringBuilder.Append(theByte.ToString("x2"));
                    }
                    fullPreviewImageMd5 = stringBuilder.ToString();
                }

                FileInfo assetFullPreviewFileInfo = new FileInfo(Path.Combine(assetsDirectoryInfo.FullName, fullPreviewImageMd5));
                FileInfo assetThumbnailFileInfo = new FileInfo(Path.Combine(assetsDirectoryInfo.FullName, tokenImageMd5));

                Image tokenImage = Image.FromFile(TokenImage.FullName);
                Image fullPreviewImage = Image.FromFile(PortraitImage.FullName);

                string fullPreviewImageString = GetImageFormatExtension(fullPreviewImage.RawFormat);
                string tokenImageString = GetImageFormatExtension(tokenImage.RawFormat);
                
                FileInfo assetFullPreviewPngFileInfo = new FileInfo(Path.Combine(assetsDirectoryInfo.FullName, string.Format("{0}.{1}", fullPreviewImageMd5, fullPreviewImageString)));
                FileInfo assetThumbnailPngFileInfo = new FileInfo(Path.Combine(assetsDirectoryInfo.FullName, string.Format("{0}.{1}", tokenImageMd5, tokenImageString)));

                File.Copy(TokenImage.FullName, thumbnailFileInfo.FullName);
                File.Copy(TokenImage.FullName, assetThumbnailPngFileInfo.FullName);
                File.Copy(PortraitImage.FullName, assetFullPreviewPngFileInfo.FullName);

                OutputPropertiesXml(propertiesXmlFileInfo);
                OutputContentXml(contentXmlFileInfo, tokenImageMd5, fullPreviewImageMd5);
                OutputImageXml(assetThumbnailFileInfo, tokenImageMd5, string.Format("{0}Thumbnail", ReplaceAll(Name, "~!@#$%^&*()_+`-={}|:\"<>?[]\\;',./ ")), tokenImageString);
                OutputImageXml(assetFullPreviewFileInfo, fullPreviewImageMd5, string.Format("{0}FullPreview", ReplaceAll(Name, "~!@#$%^&*()_+`-={}|:\"<>?[]\\;',./ ")), fullPreviewImageString);

                temporaryRar = new FileInfo(Path.Combine(Path.GetTempPath(), Path.GetTempFileName() + ".zip"));
                using (ZipFile zipFile = new ZipFile())
                {
                    foreach (FileInfo fileInfo in tokenDirectoryInfo.GetFiles())
                    {
                        zipFile.AddFile(fileInfo.FullName, "");
                    }
                    foreach (FileInfo fileInfo in assetsDirectoryInfo.GetFiles())
                    {
                        zipFile.AddFile(fileInfo.FullName, "assets");
                    }
                    zipFile.Save(temporaryRar.FullName);
                }

                temporaryRar.CopyTo(outputFileInfo.FullName, true);
                returnBool = true;
            }
            finally
            {
                if (temporaryRar != null && temporaryRar.Exists)
                {
                    try { temporaryRar.Delete(); }
                    catch (Exception) { }
                }
                if (tokenDirectoryInfo != null && tokenDirectoryInfo.Exists)
                {
                    try { tokenDirectoryInfo.Delete(true); }
                    catch (Exception) { }
                }
            }

            return returnBool;
        }

        private string ReplaceAll(string originalString, string replaceCharacters)
        {
            string newString = originalString;
            foreach (char character in replaceCharacters.ToCharArray())
            {
                newString = newString.Replace(character.ToString(), "");
            }
            return newString;
        }

        private string GetImageFormatExtension(ImageFormat imageFormat)
        {
            if (imageFormat.Equals(ImageFormat.Bmp))
            {
                return "bmp";
            }
            else if (imageFormat.Equals(ImageFormat.Gif))
            {
                return "gif";
            }
            else if (imageFormat.Equals(ImageFormat.Jpeg))
            {
                return "jpeg";
            }
            else if (imageFormat.Equals(ImageFormat.Png))
            {
                return "png";
            }
            else if (imageFormat.Equals(ImageFormat.Tiff))
            {
                return "tiff";
            }
            return null;
        }

        private bool OutputImageXml(FileInfo fileInfo, string id, string name, string imageFormat)
        {
            XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
            xmlWriterSettings.CloseOutput = true;
            xmlWriterSettings.OmitXmlDeclaration = true;
            xmlWriterSettings.Encoding = new UTF8Encoding(false);
            xmlWriterSettings.Indent = true;
            xmlWriterSettings.IndentChars = "  ";
            xmlWriterSettings.NewLineChars = "\r\n";

            using (XmlWriter xmlWriter = XmlWriter.Create(fileInfo.FullName, xmlWriterSettings))
            {
                xmlWriter.WriteStartElement("net.rptools.maptool.model.Asset");
                xmlWriter.WriteStartElement("id");
                xmlWriter.WriteKeyValuePair("id", id);
                xmlWriter.WriteEndElement(); //id
                xmlWriter.WriteKeyValuePair("name", name);
                xmlWriter.WriteKeyValuePair("extension", imageFormat);
                xmlWriter.WriteStartElement("image");
                xmlWriter.WriteEndElement(); //image
                xmlWriter.WriteEndElement();
            }
            string info = string.Empty;
            using (FileStream fileStream = File.OpenRead(fileInfo.FullName))
            using (StreamReader streamReader = new StreamReader(fileStream))
            {
                info = streamReader.ReadToEnd();
            }
            info = info.Replace(@"<image />", @"<image/>");
            using (FileStream fileStream = File.Open(fileInfo.FullName, FileMode.Create))
            using (StreamWriter streamWriter = new StreamWriter(fileStream))
            {
                streamWriter.Write(info);
            }
            return true;
        }

        private bool OutputPropertiesXml(FileInfo fileInfo)
        {
            XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
            xmlWriterSettings.CloseOutput = true;
            xmlWriterSettings.OmitXmlDeclaration = true;
            xmlWriterSettings.Encoding = new UTF8Encoding(false);
            xmlWriterSettings.Indent = true;
            xmlWriterSettings.IndentChars = "  ";
            xmlWriterSettings.NewLineChars = "\r\n";

            using (XmlWriter xmlWriter = XmlWriter.Create(fileInfo.FullName, xmlWriterSettings))
            {
                xmlWriter.WriteStartElement("map");
                xmlWriter.WriteStartElement("entry");

                xmlWriter.WriteKeyValuePair("string", "version");
                xmlWriter.WriteKeyValuePair("string", Version);

                xmlWriter.WriteEndElement();
                xmlWriter.WriteEndElement();
            }
            return true;
        }

        private bool OutputContentXml(FileInfo fileInfo, string tokenGuid, string portraitGuid)
        {
            XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
            xmlWriterSettings.CloseOutput = true;
            xmlWriterSettings.OmitXmlDeclaration = true;
            xmlWriterSettings.Indent = true;
            xmlWriterSettings.IndentChars = "  ";
            xmlWriterSettings.NewLineChars = "\r\n";
            xmlWriterSettings.Encoding = new UTF8Encoding(false);

            try
            {
                using (XmlWriter xmlWriter = XmlWriter.Create(fileInfo.FullName, xmlWriterSettings))
                {
                    xmlWriter.WriteStartElement("net.rptools.maptool.model.Token");
                    OutputMainId(xmlWriter);
                    xmlWriter.WriteKeyValuePair("beingImpersonated", BeingImpersonated.ToString().ToLower());
                    OutputImageAssetMap(xmlWriter, tokenGuid);
                    OutputStatics(xmlWriter);
                    if (!string.IsNullOrEmpty(SizeMapGuid))
                    {
                        OutputSizeMap(xmlWriter, SizeMapGuid);
                    }
                    OutputSecondaryVariables(xmlWriter, portraitGuid);
                    OutputStates(xmlWriter);
                    OutputPropertyMap(xmlWriter);
                    xmlWriter.WriteStartElement("macroPropertiesMap");
                    xmlWriter.WriteEndElement();
                    xmlWriter.WriteStartElement("speechMap");
                    xmlWriter.WriteEndElement();
                    xmlWriter.WriteEndElement();
                }
                return true;
            }
            catch (SecurityException ex)
            {
                mLog.Warn("Could not output content xml, could not edit the file.", ex);
                return false;
            }
        }

        private bool OutputStatics(XmlWriter xmlWriter)
        {
            try
            {
                xmlWriter.WriteKeyValuePair("x", X);
                xmlWriter.WriteKeyValuePair("y", Y);
                xmlWriter.WriteKeyValuePair("z", Z);
                xmlWriter.WriteKeyValuePair("anchorX", AnchorX);
                xmlWriter.WriteKeyValuePair("anchorY", AnchorY);
                xmlWriter.WriteKeyValuePair("sizeScale", SizeScale);
                xmlWriter.WriteKeyValuePair("lastX", LastX);
                xmlWriter.WriteKeyValuePair("lastY", LastY);
                xmlWriter.WriteKeyValuePair("snapToScale", SnapToScale.ToString().ToLower());
                xmlWriter.WriteKeyValuePair("width", Width);
                xmlWriter.WriteKeyValuePair("height", Height);
                xmlWriter.WriteKeyValuePair("scaleX", ScaleX);
                xmlWriter.WriteKeyValuePair("scaleY", ScaleY);
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("Could not add property to xml", ex);
                return false;
            }

            return true;
        }

        private bool OutputSecondaryVariables(XmlWriter xmlWriter, string portraitGuid)
        {
            try
            {
                xmlWriter.WriteKeyValuePair("snapToGrid", SnapToGrid.ToString().ToLower());
                xmlWriter.WriteKeyValuePair("isVisible", IsVisible.ToString().ToLower());
                xmlWriter.WriteKeyValuePair("visibleOnlyToOwner", VisibleOnlyToOwner.ToString().ToLower());
                xmlWriter.WriteKeyValuePair("name", Name);

                if (!OutputOwnerList(xmlWriter))
                {
                    return false;
                }

                xmlWriter.WriteKeyValuePair("ownerType", OwnerType);
                xmlWriter.WriteKeyValuePair("tokenShape", TokenShape);
                xmlWriter.WriteKeyValuePair("tokenType", TokenType.ToString());
                xmlWriter.WriteKeyValuePair("layer", TokenLayer);
                xmlWriter.WriteKeyValuePair("propertyType", PropertyType);
                xmlWriter.WriteKeyValuePair("isFlippedX", IsFlippedX.ToString().ToLower());
                xmlWriter.WriteKeyValuePair("isFlippedY", IsFlippedY.ToString().ToLower());

                if (!OutputPortraitImage(xmlWriter, portraitGuid))
                {
                    return false;
                }

                xmlWriter.WriteKeyValuePair("sightType", SightType ?? "Normal");
                xmlWriter.WriteKeyValuePair("hasSight", HasSight.ToString().ToLower());
                xmlWriter.WriteKeyValuePair("label", Label ?? string.Empty);
                xmlWriter.WriteKeyValuePair("notes", Notes ?? string.Empty);
                xmlWriter.WriteKeyValuePair("gmNotes", GMNotes ?? string.Empty);
                xmlWriter.WriteKeyValuePair("gmName", GMName ?? string.Empty);
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("OutputSecondaryVariables: Unable to write key", ex);
                return false;
            }
            return true;
        }

        private bool OutputOwnerList(XmlWriter xmlWriter)
        {
            try
            {
                xmlWriter.WriteStartElement("ownerList");
                xmlWriter.WriteKeyValuePair("string", Name);
                xmlWriter.WriteEndElement();
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("OutputOwnerList: Unable to write key", ex);
                return false;
            }
            return true;
        }



        private bool OutputSizeMap(XmlWriter xmlWriter, string sizeGuid)
        {
            try
            {
                xmlWriter.WriteStartElement("sizeMap");
                xmlWriter.WriteStartElement("entry");
                xmlWriter.WriteKeyValuePair("java-class", JavaClass);
                xmlWriter.WriteStartElement("net.rptools.maptool.model.GUID");
                xmlWriter.WriteKeyValuePair("baGUID", sizeGuid);
                xmlWriter.WriteEndElement();
                xmlWriter.WriteEndElement();
                xmlWriter.WriteEndElement();
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("OutputSizeMap: Could not add keys", ex);
                return false;
            }
            return true;
        }

        private bool OutputPortraitImage(XmlWriter xmlWriter, string guid)
        {
            try
            {
                xmlWriter.WriteStartElement("portraitImage");
                xmlWriter.WriteKeyValuePair("id", guid);
                xmlWriter.WriteEndElement();
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("Could not add keys to Xml", ex);
                return false;
            }
            return true;
        }

        private bool OutputImageAssetMap(XmlWriter xmlWriter, string guid)
        {
            try
            {
                xmlWriter.WriteStartElement("imageAssetMap");
                xmlWriter.WriteStartElement("entry");
                xmlWriter.WriteStartElement("null");
                xmlWriter.WriteEndElement(); //null
                xmlWriter.WriteStartElement("net.rptools.lib.MD5Key");
                xmlWriter.WriteKeyValuePair("id", guid);
                xmlWriter.WriteEndElement(); //net.rptools.lib.MD5Key
                xmlWriter.WriteEndElement(); //entry
                xmlWriter.WriteEndElement(); //imageAssetMap
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("OutputImageAssetMap: Could not add keys to Xml", ex);
                return false;
            }
            return true;
        }

        private bool OutputMainId(XmlWriter xmlWriter)
        {
            try
            {
                xmlWriter.WriteStartElement("id");
                xmlWriter.WriteKeyValuePair("baGUID", mBaGUID);
                xmlWriter.WriteEndElement(); //id
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("OutputMainId: Could not add keys to Xml", ex);
                return false;
            }
            return true;
        }

        private bool OutputStates(XmlWriter xmlWriter)
        {
            try
            {
                xmlWriter.WriteStartElement("state");
                foreach (State state in States ?? new List<State>())
                {
                    xmlWriter.WriteStartElement("entry");
                    xmlWriter.WriteKeyValuePair("string", state.Name);
                    xmlWriter.WriteKeyValuePair("boolean", state.Value);
                    xmlWriter.WriteEndElement(); //entry
                }
                xmlWriter.WriteEndElement(); //State
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("Could not add States to Xml", ex);
                return false;
            }
            return true;
        }

        private bool OutputPropertyMap(XmlWriter xmlWriter)
        {
            try
            {
                xmlWriter.WriteStartElement("propertyMap");
                xmlWriter.WriteStartElement("store");
                foreach (MaptoolsProperty property in Properties ?? new List<MaptoolsProperty>())
                {
                    xmlWriter.WriteStartElement("entry");
                    xmlWriter.WriteKeyValuePair("string", property.Name.ToLower());
                    xmlWriter.WriteStartElement("net.rptools.CaseInsensitiveHashMap_-KeyValue");
                    xmlWriter.WriteKeyValuePair("key", property.Name);
                    xmlWriter.WriteStartElement("value");
                    xmlWriter.WriteAttributeString("class", "string");
                    xmlWriter.WriteValue(property.Value);
                    xmlWriter.WriteEndElement(); //value
                    xmlWriter.WriteStartElement("outer-class");
                    xmlWriter.WriteAttributeString("reference", "../../../..");
                    xmlWriter.WriteEndElement(); //outer-class
                    xmlWriter.WriteEndElement(); //net.rptools.CaseInsensitiveHashMap_-KeyValue
                    xmlWriter.WriteEndElement(); //entry
                }
                xmlWriter.WriteEndElement(); //store
                xmlWriter.WriteEndElement(); //propertyMap
            }
            catch (InvalidOperationException ex)
            {
                mLog.Warn("Could not add States to Xml", ex);
                return false;
            }
            return true;
        }


        private static ILog mLog = LogManager.GetLogger("Bridge.Exporter.Maptools");

        private const bool BeingImpersonated = false;
        private const bool SnapToScale = true;
        private const bool SnapToGrid = true;
        private const bool IsVisible = true;
        private const bool VisibleOnlyToOwner = false;
        private const bool IsFlippedX = false;
        private const bool IsFlippedY = false;

        private const int OwnerType = 0;
        private const int X = 50;
        private const int Y = 650;
        private const int Z = 140;
        private const int AnchorX = 0;
        private const int AnchorY = 0;
        private const int LastX = 0;
        private const int LastY = 0;
        private const int Width = 128;
        private const int Height = 128;
        private const int ExecuteMaximumTime = 5000;

        private const string SizeScale = "1.0";
        private const string ScaleX = "1.0";
        private const string ScaleY = "1.0";
        private const string TokenShape = "SQUARE";
        private const string TokenLayer = "TOKEN";
        private const string Version = "1.3.b76";
        private const string JavaClass = "net.rptools.maptool.model.SquareGrid";

        private static ImageFormat ImageFormat = ImageFormat.Png;

        private string mBaGUID = Guid.NewGuid().ToString().Replace("-", "");
    }
}
